package com.ease.util {
   import com.ease.util.events.DataEvent;
   import com.ease.util.logging.LogManager;

   import flash.events.Event;
   import flash.events.EventDispatcher;
   import flash.events.TimerEvent;
   import flash.utils.Timer;
   import flash.utils.getTimer;

   import mx.logging.ILogger;
   import mx.utils.StringUtil;

   [Event(name="{com.ease.flexutil.PseudoThread.THREAD_TERMINATED_EVENT}",
       type="com.ease.util.events.DataEvent")]
   /**
    * This class is influenced by the following article:
    * http://blogs.adobe.com/aharui/2008/01/threads_in_actionscript_3.html.
    *
    * The <code>PseudoThread</code> class is designed to call a function for a small
    * amount of time. Then release the control for another small amount of time.
    *
    * The executed function is called a <code>worker</code> function. It is expected to
    * take only one parameter called <code>thread context</code>. It needs to complete its
    * execution relatively fast. If there is no more work that needs to be done it should
    * return <code>true</code>. If there is more work that needs to be done it can record
    * its state in the <code>threadContext</code> parameter and return <code>false</code>.
    * The next time it is called, it can read its state back from the
    * <code>threadContext</code> and do another small portion of work.
    *
    * Upon <code>worker</code> completion, <code>PseudoThread</code> dispatches
    * <code>PseudoThread.THREAD_TERMINATED_EVENT</code>.
    *
    * Worker example:
    * <listing version="3.0">
    * class MyThreadContext {
    *    var workItems:Array = [];
    * }
    *
    * function myWorker(context:MyThreadContext):Boolean {
    *    if (context.workItems.length <= 0) {
    *       // No more work items, no more work to do.
    *       return true;
    *    }
    *
    *    var workItem:MyWorkItemt = context.workItem.pop() as MyWorkItem;
    *    // Do some processing of the work item.
    *    ...
    *    // Based on the processing above generate more work items and
    *    // push them in context.workItems array
    *    ...
    *    // Return false indicating that you have not finished with processing.
    *    return false;
    * }
    * </listing>
    *
    * Usage example:
    * <listing version="3.0">
    * {
    *    ...
    *    // Prepare the initial context and the first work item.
    *    var context:MyThreadContext = new MyThreadContext();
    *    context.workItems.push(new MyWorkItem(...));
    *    ...
    *    // Create the pseudo thread object
    *    var pt:PseudoThread = new PseudoThread(100, 1, myWorker, context);
    *    // Start it
    *    pt.start();
    *    ...
    * }
    * </listing>
    *
    * In order to get the result of <code>myWorker</code> function
    * execution you need to subscribe for the
    * <code>PseudoThread.THREAD_TERMINATED_EVENT</code> event.
    * The event is of type <code>com.ease.flexutil.events.DataEvent</code> and
    * the <code>DataEvent.data</code> property will contain the <code>threadContext</code>.
    *
    * <listing version="3.0">
    * class MyThreadContext {
    *    var workItems:Array = [];
    *    // Your worker thread will store the result of its work here:
    *    var result:Object;
    * }
    * ...
    * // Now update the myWorker code a bit.
    * function myWorker(context:MyThreadContext):Boolean {
    *    if (context.workItems.length <= 0) {
    *       // No more work items, no more work to do.
    *       context.result = ...;
    *       return true;
    *    }
    *    ...
    * }
    * ...
    * ...
    * // When creating the thread object, do this:
    * var context:MyThreadContext = new MyThreadContext();
    * ...
    * // Create the pseudo thread object
    * var pt:PseudoThread = new PseudoThread(100, 1, myWorker, context);
    * // Subscribe for the thread termination
    * pt.addEventListener(PseudoThread.THREAD_TERMINATED_EVENT, onThreadCompleted);
    * // Start it
    * pt.start();
    * ...
    * ...
    * // Here is our event handler where we will get the result
    * function onThreadCompleted(event:DataEvent):void {
    *    // NOTE: You may want to unsubscribe from the event queue first depending on the
    *    // lifetime of your PseudoThread object.
    *    PseudoThread(event.target).removeEventListener(
    *          PseudoThread.THREAD_TERMINATED_EVENT, arguments.callee);
    *
    *    // This one will be called at some point
    *    var result:Object = MyThreadContext(event.data).result;
    *
    *    // Do something with the result.
    *    ...
    * }
    * </listing>
    *
    * <p/> Sometimes an exception may happen in the <code>worker</code>
    * function which is not handled by the <code>worker</code> internally.
    * In this case the <code>PseudoThread</code> is terminated.
    * This is referred as <strong>abnormal</strong> termination.
    * The behaviour in this case is as follows:
    * <ul>
    *    <li>If <code>threadContext</code> was not provided to <code>PseudoThread</code>,
    *       then <code>PseudoThread.THREAD_TERMINATED_EVENT</code> is dispatched as usual
    *       but the <code>DataEvent.data</code> property will contain
    *       the <code>Error</code> instance describing the exception instead of being
    *       <code>null</code>.</li>
    *    <li>If you have provided a <code>threadContext</code> object and if it has
    *       a public property (or setter) marked with <strong>[InjectableError]</strong>
    *       or <strong>[Error]</strong> tag, the exception <code>Error</code> instance will
    *       be assigned to this property and the
    *       <code>PseudoThread.THREAD_TERMINATED_EVENT</code> will be dispatched
    *       as usual.</li>
    *    <li>If neither of the above conditions is met, then <code>PseudoThread</code>
    *       will simply propagate the exception upwards the stack trace.</li>
    * </ul>
    *
    * @see com.vwmare.flexutil.events.DataEvent
    */
   public class PseudoThread extends EventDispatcher {

      private static const _logger:ILogger = LogManager.getLogger(PseudoThread);

      private static const INJECTABLE_ERROR_TAG:String = "InjectableError|Error";

      /**
       * The name of the event thrown when <code>PseudoThread</code> worker
       * function completes its execution. The event type is <code>DataEvent</code>
       * where <code>DataEvent.data</code> will contain the <code>threadContext</code>
       *
       * @see com.ease.flexutil.events.DataEvent
       * @see #threadContext
       */
      public static const THREAD_TERMINATED_EVENT:String = "pseudoThreadTerminated";

      /**
       * Keeps tack of the number of instantiated pseudo thread objects. Helps
       * assign unique id to each pseudo thread.
       */
      private static var _refCounter:uint = 0;

      /**
       * This object is used to schedule the <code>_worker</code> execution times.
       */
      private var _timer:Timer;

      private var _sleepInterval:uint;

      /**
       * @private
       */
      public function get sleepInterval():uint {
         return _sleepInterval;
      }

      /**
       * How long to sleep (in milliseconds) before calling again the worker function.
       *
       * Put 1 here, if you want <code>worker</code> function to be
       * called at the beginning of each frame. You can specify bigger values
       * if you want to give the main thread more free time to do other work.
       */
      public function set sleepInterval(value:uint):void {
         _sleepInterval = value;
         if (_timer != null) {
            _timer.delay = value;
         }
      }

      /**
       * The amount of time this thread can work without being interrupted.
       * If you specify <strong>0</strong> value this means that it is allowed to work
       * as long as it needs, effectively making the thread synchronous.
       */
      public var workInterval:uint;

      /**
       * Wheter the thread is working in synchronous or asynchronous mode.
       */
      private function get _isSynchronous():Boolean {
         return workInterval == 0;
      }

      private var _isSuspended:Boolean;

      /**
       * Indicates whether this thread is suspended.
       * A thread in the suspended state does not call its worker function.
       */
      public function get isSuspended():Boolean {
         return _isSuspended;
      }

      /** @private */
      public function set isSuspended(value:Boolean):void {
         if (value == _isSuspended) {
            return;
         }
         _isSuspended = value;
         if (_isSuspended) {
            stopTimer();
         } else {
            startTimer();
         }
      }

      /**
       * The function to call on each cycle of the <code>PseudoThread</code>.
       *
       * The function should take only one parameter. By convention
       * we call this parameter <code>thread context<code>. It needs to do a small
       * amount of work and return <code>false</code> if it is not finished and needs
       * to be called once more. It needs to return <code>true</code> if it has
       * completed its work and no more invocations are necessary. The amount of work
       * being done needs to be small cause there is no way to forcefully interrupting
       * the function. If needs to voluntarily give up on the CPU and let other
       * actionscript code also be executed.
       */
      public var worker:Function;

      /**
       * The context to pass to the worker function on each invocation.
       *
       * An object which is passed to the <code>worker</code> function each time it is
       * called. The worker may use it store some data between each of its invocations,
       * thus keeping its working state and intermediate and final results.
       */
      public var threadContext:Object;

      /**
       * Set true to only process one work item per cycle.
       */
      public var singleWorkPerCycle:Boolean = false;

      /**
       * uint id of this pseudo thread. Automatically initialized.
       */
      private var _uid:uint;

      /**
       * The id of the pseudo thread.
       */
      private var _id:String;

      /**
       * @private
       */
      public function get id():String {
         return _id;
      }

      /**
       * A string identifying the pseudo thread. Mostly useful for debug purposes.
       * If you set it to <code>null</code> value, a unique numeric value
       * will be assigned as thread id.
       */
      public function set id(value:String):void {
         _id = (value == null) ? _uid.toString() : value;
      }

      /**
       * A counter which track how many times the worker function was called before
       * it completed its job (returned true).
       */
      private var _cyclesCounter:Number;

      /**
       * A counter which track how many times the onTime function was called
       * (now many chunks of work the PseudoThread did) before the worker
       * completed its job (returned true).
       */
      private var _timerCounter:Number;

      /**
       * Initialize an instance of this class.
       *
       * @param workInterval
       *    The amount of time this thread can work without being interrupted. If you
       *    specify <strong>0</strong> value this means that it can works as long as
       *    it needs, effectively making the thread synchronous.
       * @param sleepInterval
       *    Pass 1 here, if you want <code>worker</code> function to be
       *    called at the beginning of each frame. You can specify bigger values
       *    if you want to give the main thread more free time to do other work.
       * @param worker
       *    Function to call. The function should take only one parameter. By convention
       *    we call this parameter <code>thread context<code>. It needs to do a small
       *    amount of work and return <code>false</code> if it is not finished and needs
       *    to be called once more. It needs to return <code>true</code> if it has
       *    completed its work and no more invocations are necessary. The amount of work
       *    being done needs to be small cause there is no way to forcefully interrupting
       *    the function. If needs to voluntarily give up on the CPU and let other
       *    actionscript code also be executed.
       * @param threadContext
       *    An object which is passed to the <code>worker</code> function each time it is
       *    called. The worker may use it store some data between each of its invocations,
       *    thus keeping its working state and intermediate and final results.
       * @param id
       *    Some string identifying the pseudo thread. Mostly useful for debug purposes.
       *    You can use the <code>id</code> property to get it later if needed. If you
       *    do no specify value, a unique numeric value will be assigned as thread id.
       */
      public function PseudoThread(workInterval:uint, sleepInterval:uint,
                                   worker:Function, threadContext:Object, id:String = null) {
         this.workInterval = workInterval;
         this.sleepInterval = sleepInterval;
         this.worker = worker;
         this.threadContext = threadContext;

         _uid = _refCounter;
         _id = (id == null) ? _uid.toString() : id;

         _refCounter++;

         _cyclesCounter = 0;
         _timerCounter = 0;
      }



      /**
       * Start the work. In other words: starts calling the <code>worker</code>
       * function specified in the constructor systematically.
       *
       * @param now
       *    Wheter to start calling the <code>worker</code> right away
       *    (<code>true</code>) or first sleep the specified
       *    time (<code>false</code>).
       *
       * @return If the computation was completed immediately (e.g. <code>now</code>
       *    parameter is <code>true</code> and <code>worker</code> did its job before
       *    it runs out of time, the value returned by this method is the same that will
       *    be in the <code>DateEvent.data</code> property of the
       *    <code>THREAD_TERMINATED_EVENT</code>. Otherwise this method will return
       *    <code>null</code>.
       *
       * @see PseudoThread
       * @see #THREAD_TERMINATED_EVENT
       */
      public function start(now:Boolean = false):Object {
         _logger.debug("Will start pseudo-thread {0} {1}",
            getThreadFullId(), now ? "now" : "next frame");

         if (now) {
            // Start right away.
            return onTimer(null);
         }

         startTimer();

         return null;
      }


      /**
       * A convenience method which calls the <code>start</code> method with
       * <code>now</code> parameter set to <code>true</code>.
       * @see #start()
       */
      public function startNow():Object {
         return start(true);
      }

      /**
       * Called by the timer each time our sleep period expires.
       *
       * Note that this method can be called from the <code>start</code> method too.
       */
      private function onTimer(event:Event):Object {
         // We stop the timer when the thread is paused, so we shouldn't get
         // here in this case.
         Diagnostics.assert(!this.isSuspended, "Invalid state in PseudoThread.onTimer.");

         // Increase the tracking counter.
         _timerCounter++;

         try {
            var end:uint = getTimer() + this.workInterval;
            while (_isSynchronous || (getTimer() < end)) {
               var workDone:Boolean = worker(this.threadContext);
               _cyclesCounter++;
               if (workDone) {
                  return terminate();
               }
               if (singleWorkPerCycle) {
                  break;
               }
               // If the thread was suspended in a worker item then
               // stop processing until it is resumed.
               if (_isSuspended) {
                  return null;
               }
            }
         } catch (e:Error) {
            return handleWorkerException(e);
         }

         startTimer();

         return null;
      }

      /**
       * @param exception
       *    An exception which happened in the worker during its execution.
       */
      private function handleWorkerException(exception:Error):Object {
         // First job is to call the termination code, but no event dispatching yet.
         var threadContext:Object = terminate(false /*dispatch*/);

         // Decide whether to fire thread termination even or re-throw the exception.
         var newContext:Object = computeNewThreadContext(threadContext, exception);
         if (newContext != null) {
            _logger.info("Worker exception for pseudo-thread {0} caught: {1}",
               getThreadFullId(), exception.getStackTrace());
            dispatchEvent(newTerminateEvent(newContext));
            return newContext;
         }

         // Couldn't compute proper context for the thread terminate event, so
         // re-throw the exception.
         throw exception;
      }

      /**
       * Compute a new threadContext object.
       *
       * The idea is that if there was no old thread context the new one will be
       * the error.  If there was threadContext, we keep it
       * and try to inject the error in it. If injecting the error fails, we
       * consider that we cannot construct proper thread context.
       */
      private function computeNewThreadContext(threadContext:Object, error:Error):Object {
         if (threadContext == null) {
            // User did not supplied threadContext, then we use the
            // error as the "threadContext".
            return error;
         }

         // Use supplied threadContext object that it will expect to get back on
         // PseudoThread termination. We try to inject the error there.
         var errorInjected:Boolean = ReflectionUtil.
            setTaggedPropertyValue(threadContext, INJECTABLE_ERROR_TAG, error);
         if (errorInjected) {
            return threadContext;
         }

         // Cannot inject the error into the threadContext, return null value.
         return null;
      }

      /**
       * Release the timer and nullifies internal references.
       * Also dispatches a <code>PseudoThread.THREAD_TERMINATED_EVENT<code> event.
       */
      private function terminate(dispatch:Boolean = true):Object {
         // Calculate the return value.
         // Since the user might have requested synchronous execution, we return the
         // the context because it makes sense that the user expects
         // to get the result right away (also simplifies the code) using
         // PseudoThread in this case.
         var retVal:Object = this.threadContext;

         // Clear the resources.
         stopTimer();

         // Dump some debug info how many cycles it took the thread to complete.
         _logger.debug("Terminated pseudo-thread {0}, chunks = {1}, cycles = {2}",
            getThreadFullId(), _timerCounter, _cyclesCounter);

         if (dispatch) {
            // Dispatch termination even to interested parties
            dispatchEvent(newTerminateEvent(retVal));
         }

         // Return
         return retVal;
      }

      /**
       * Creates the timer if necessary, and starts it.
       */
      private function startTimer():void {
         if (_timer == null) {
            _timer = new Timer(this.sleepInterval, 1/*count*/);
            _timer.addEventListener(TimerEvent.TIMER, onTimer);
         } else {
            _timer.reset();
         }
         _timer.start();
      }

      /**
       * Stops the timer.
       */
      private function stopTimer():void {
         if (_timer == null) {
            return;
         }
         _timer.stop();
         _timer.removeEventListener(TimerEvent.TIMER, onTimer);
         _timer = null;
      }

      /**
       * A helper method that construct pseudo-thread id suitable for logging.
       */
      private function getThreadFullId():String {
         // Substitue is as efficient as concatenation, but much more readable.
         return StringUtil.substitute("#{0} ({1})", _uid, _id);
      }

      /**
       * A convenience factory method to create an instance of this class that represents
       * the termination of an existing <code>PseudoThread</code>.
       */
      private static function newTerminateEvent(threadContext:Object = null):DataEvent {
         return new DataEvent(THREAD_TERMINATED_EVENT, threadContext);
      }

   } // class
} // package
